﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DeepZoomPivotConstructor.ErrorAndLoggingInterfaces;
using DeepZoomPivotConstructor.Utilities;
using DeepZoomPivotConstructor.VisualLib;
using DeepZoomPivotConstructor.VisualLib.Hotspots;
using DeepZoomPivotConstructor.VisualLib.Utilities;
using Microsoft.Win32;
using ProcessUtilities;
using RenderEngineInterfaces;

namespace GSRender
{
    /// <summary>
    /// Runs ghostview to render to jpegs. Once done it bundles them up into a big-ass nice
    /// looking thing and generates a nice big fat visual for them. Yes!
    /// </summary>
    [Export(typeof(IRenderItem))]
    [RenderInfo(typeof(FileInfo))]
    public class RenderUsingGS : IRenderItem, IPartImportsSatisfiedNotification
    {
        /// <summary>
        /// How many dots per inch should we be rendering
        /// at?
        /// </summary>
        [Import("RenderingDPI", typeof(int), AllowDefault = true)]
        public int DotsPerInch { get; set; }

        /// <summary>
        /// How much space between slides when we do the packing?
        /// </summary>
        public double YBoarderSize { get; set; }

        /// <summary>
        /// What is the ghost script executable location? Filled with
        /// a default if not specified.
        /// </summary>
        public string GSLocation { get; set; }

        /// <summary>
        /// Set to true if you want to use the talk directory for a temp
        /// directory. This is cool if you are rendering files in a temp
        /// dir already - you can keep everything together and when you go
        /// and look at them they are all in one place.
        /// </summary>
        public bool UseTalkDirForTemp { get; set; }

        /// <summary>
        /// The local render engine - so we can render jpeg's, etc.
        /// </summary>
        [Import]
        private IRenderEngine RenderEngine { get; set; }

        /// <summary>
        /// Help for us to layout everything
        /// </summary>
        [Import]
        private ILayoutHelper LayoutHelper { get; set; }

        /// <summary>
        /// Max # of pages we will render - keeps memory under control.
        /// </summary>
        private const int MaxPages = 60;

        /// <summary>
        /// Get/Set the timeout in miliseconds before we will kill a gs process that hasn't updated a file.
        /// </summary>
        public int TimeToLetGSRun { get; set; }

        /// <summary>
        /// If we should be using a different temp directory than the one that is there by default... :-)
        /// </summary>
#pragma warning disable 0649
        [Import("RenderingTempDir", AllowDefault = true)]
        private DirectoryInfo RenderingTempDir;
#pragma warning restore 0649

        /// <summary>
        /// Make sure we don't try to run too many versions of GS at once!
        /// </summary>
        private static Semaphore _RenderLimiter = new Semaphore(3, 3);

        /// <summary>
        /// Init all of our various values to default
        /// </summary>
        public RenderUsingGS()
        {
            YBoarderSize = 0.2;
            UseTalkDirForTemp = false;
            TimeToLetGSRun = 2 * 60 * 1000;
            CacheGS();
        }

        /// <summary>
        /// Called after we've been setup by the part guy. We check to make sure that
        /// all imported values make sense and aren't totally bogus. I.E. we have some default
        /// values, and cross check to make sure they were filled, or put in default values.
        /// </summary>
        public void OnImportsSatisfied()
        {
            if (DotsPerInch == 0)
            {
                DotsPerInch = 150;
            }
        }

        /// <summary>
        /// Create a future that will do the rendering.
        /// </summary>
        /// <param name="what"></param>
        /// <returns></returns>
        public Task<Visual> Render(object what)
        {
            FileInfo f = what as FileInfo;
            if (f == null)
            {
                return null;
            }

            return Task<Visual>.Factory.StartNew(() =>
            {
                return RenderAFile(f);
            });
        }

        /// <summary>
        /// Send messages around
        /// </summary>
        [Import]
        private IErrorLogger MyErrorLogger { get; set; }

        /// <summary>
        /// Render a file to a visual hierarchy
        /// </summary>
        /// <param name="talkInfo"></param>
        /// <returns></returns>
        private Visual RenderAFile(FileInfo talkInfo)
        {
            MyErrorLogger.LogMessage("RenderUsingGS", "Starting rendering of " + talkInfo.FullName);

            talkInfo.Refresh();
            if (!talkInfo.Exists)
            {
                MyErrorLogger.LogMessage("RenderUsingGS", string.Format("File '{0}' does not exist!", talkInfo.FullName));
                return null;
            }

            var imageFiles = RenderToPictureFiles(talkInfo);
            if (imageFiles.Count == 0)
            {
                return null;
            }

            ///
            /// If this was a PS file, there may be some custom rotation we have to do
            /// 

            string ext = talkInfo.Extension.ToLower();
            if (ext == ".ps" || ext == ".eps")
            {
                RotatePoscriptResults(talkInfo, imageFiles);
            }

            ///
            /// Box those up with the default jpeg (or whatever) rendering dude
            /// Note that we have to preserve order here, so we don't do this
            /// in parallel, sadly!
            /// 

            var allfutures = from f in imageFiles
                             select RenderEngine.RenderToFuture(f);
            var allfuturesdone = allfutures.ToArray(); /// Forces them into existance, creates the futures.
            var allRawVisuals = from v in allfuturesdone
                                where v != null
                                let r = v.Result
                                where r != null
                                select r;

            ///
            /// This is the final wrapper for this talk
            /// 

            VisualHolder vholder = new VisualHolder();

            ///
            /// Wrap the visuals in centering hot spots. If there is nothing
            /// to do -- that is, if there is a single image, then we skip this next
            /// step.
            /// 

            Visual[] allVisuals = null;
            if (allRawVisuals.Count() == 1)
            {
                allVisuals = (from v in allRawVisuals
                              select v.WrapInCenterHereHS()).ToArray();
            }
            else
            {
                allVisuals = (from v in new VisualSeqenceWrapper(allRawVisuals, vholder)
                              select v).ToArray();
            }

            ///
            /// Now lay them out in a nice 2x3 way
            ///

            Visual vis = LayoutHelper.LayoutIn2By3Rows(allVisuals);

            ///
            /// Next, do the master page, if needed. If we fail to render that first
            /// page, then we don't care, and just go...
            /// 

            bool doneTitle = false;
            if (allVisuals.Count() > 1)
            {
                var page1Raw = RenderEngine.Render(imageFiles[0]);
                if (page1Raw != null)
                {
                    var vhs = page1Raw.CreateVHS();
                    vhs.Add(new HSActionCenterHere());
                    vhs.Add(new HSActionCenterThere(allVisuals[0]) { KeysToActivate = HotSpotActionBase.ActivationKeys.KeyDownArrow });
                    var page1 = page1Raw.WrapWithVisuals(new List<Visual>()
                        {
                            vhs
                        });

                    vis.IncZIndex();
                    page1.ScaleToWidth(vis.Width);
                    vis.YPosition = page1.Height + YBoarderSize;

                    vholder.AddImage(vis);
                    vholder.AddImage(page1);
                    vis = vholder;
                    doneTitle = true;
                }
            }

            if (!doneTitle)
            {
                vholder.AddImage(vis);
            }

            vholder.Info = new Visual.VisualInfo();
            vholder.Info.Title = talkInfo.Name.Substring(0, talkInfo.Name.Length - talkInfo.Extension.Length);

            return vholder;
        }

        /// <summary>
        /// Postscript files can be a bit ugly - mostly because ghostscript isn't very good about
        /// getting the talk orientation right. So we have to recover sometimes. That is what this
        /// guy does.
        /// </summary>
        /// <param name="talkInfo">FileInfo pointer to a post-script file</param>
        /// <param name="imageFiles">The list of images that were rendered.</param>
        private void RotatePoscriptResults(FileInfo talkInfo, List<FileInfo> imageFiles)
        {
            ///
            /// To guess the orientation, we have to do a little work here. The main problem is
            /// that how PostScript files put things together is a mixed bag.
            /// 
            /// We look for two clues. First, the BoundingBox - if that has x longer than y, then
            /// we can "assume" we are dealing with a landscaped file. If the Orientation comment exists,
            /// and is marked landscape, we again make the same assumption.
            /// 

            var psCommentLines = talkInfo.GetLineIterator().TakeWhile(ln => !ln.Contains("%%EndComments"));
            var orientLines = from l in psCommentLines
                              where l.Contains("Orientation") || l.Contains("BoundingBox")
                              select l;

            bool isLandscapeBB = false;
            bool isLandscapeOr = false;

            foreach (var l in orientLines)
            {
                if (l.Contains("Orientation") && l.Contains("Landscape"))
                {
                    isLandscapeOr = true;
                }
                if (l.Contains("BoundingBox"))
                {
                    var splitinfo = l.Split(' ');
                    var allints = from i in l.Split(' ')
                                  where isOnlyNumbers(i)
                                  select Convert.ToInt32(i);

                    var allintsa = allints.ToArray();
                    if (allintsa.Length == 4)
                    {
                        int deltax = allintsa[2] - allintsa[0];
                        int deltay = allintsa[3] - allintsa[1];

                        isLandscapeBB = deltax > deltay;
                    }
                }
            }

            bool isLandscape = isLandscapeBB || isLandscapeOr;
#if false
            ///
            /// Ghost script will have done portrait. Look at the source ps file to
            /// see if the orientation is land-scape.
            /// 

            var psCommentLines = talkInfo.GetLineIterator().TakeWhile(ln => !ln.Contains("%%EndComments"));
            var orientLines = from l in psCommentLines
                                where l.Contains("Orientation")
                                select l;

            bool isLandscape = false;
            if (orientLines.Count() > 0)
            {
                isLandscape = orientLines.First().Contains("Landscape");
            }
#endif

            ///
            /// Now, check out which orientation it is in. See if it matches.
            /// 

            if (isLandscape != Image.IsLandscape(imageFiles[0]))
            {
                ///
                /// We rotate one direction for all ps except that generated by latex's dvips. :(
                /// 

                bool rotate90 = true;
                var dvilines = from l in psCommentLines
                               where l.Contains("Creator")
                               select l;
                if (dvilines.Count() > 0)
                {
                    rotate90 = !dvilines.First().Contains("dvips");
                }

                foreach (var image in imageFiles)
                {
                    Image.RotateInPlace(image, rotate90);
                }
            }
        }

        /// <summary>
        /// Returns true if only the integer numbers are in this string.
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        private bool isOnlyNumbers(string str)
        {
            var badchars = from c in str
                           where !char.IsDigit(c)
                           select c;
            return badchars.Count() == 0 & str.Length != 0;
        }

        /// <summary>
        /// We are running on a background thread somewhere. We do the actual rendering of a file using ghost script.
        /// </summary>
        /// <param name="f"></param>
        /// <returns></returns>
        private List<FileInfo> RenderToPictureFiles(FileInfo talkInfo)
        {
            ///
            /// We will be storing all of our output files in a special
            /// directory (unless it is the talk directory - which happens
            /// under certian circumstances).
            /// 

            DirectoryInfo baseDirForFiles = talkInfo.Directory;
            string talkName = talkInfo.Name.Substring(0, talkInfo.Name.Length - talkInfo.Extension.Length);
            if (!UseTalkDirForTemp)
            {
                string tempDir = Path.GetTempPath();
                if (RenderingTempDir != null)
                {
                    tempDir = RenderingTempDir.FullName;
                }

                baseDirForFiles = new DirectoryInfo(tempDir + "\\" + talkName);
                if (!baseDirForFiles.Exists)
                {
                    baseDirForFiles.Create();
                }
            }

            ///
            /// Build up the various strings and files that we are goign to be using.
            /// 

            FileInfo tokenInfo = new FileInfo(baseDirForFiles.FullName + "\\" + talkName + "-done.txt");
            FileInfo logInfo = new FileInfo(baseDirForFiles.FullName + "\\" + talkName + "-log.txt");
            FileInfo filelistInfo = new FileInfo(baseDirForFiles.FullName + "\\" + talkName + "-filelist.txt");

            ///
            /// Spec out the process we will start.
            /// 

            ProcessStartInfo sinfo = new ProcessStartInfo();
            sinfo.FileName = GSLocation;
            sinfo.Arguments = "-sDEVICE=png16m "
                + "-r" + DotsPerInch.ToString() + "x" + DotsPerInch.ToString() + " "
                + "\"-sOutputFile=" + talkName + "-%03d.png\" "
                + "-dBATCH -dNOPAUSE ";
            if (IsEPSFile(talkInfo))
            {
                sinfo.Arguments += " -dEPSCrop";
            }
            sinfo.Arguments += " \"" + talkInfo.FullName + "\"";
            sinfo.CreateNoWindow = true;
            sinfo.RedirectStandardOutput = true;
            sinfo.UseShellExecute = false;
            sinfo.WorkingDirectory = baseDirForFiles.FullName;

            ///
            /// Now start the process. We switch the error mode
            /// so that when gs crashes it doesn't pop the dialog
            /// box. And because this is a process wide setting, we need
            /// to make sure two people don't try to start the process
            /// at once.
            /// 

            string error_seen = "";
            DateTime startime, endtime;

            _RenderLimiter.WaitOne();
            try
            {
                startime = DateTime.Now;
                Process gsproc = null;

                lock (this)
                {
                    using (ChangeErrorMode newerr = new ChangeErrorMode(ChangeErrorMode.ErrorModes.FailCriticalErrors | ChangeErrorMode.ErrorModes.NoGpFaultErrorBox))
                    {
                        gsproc = Process.Start(sinfo);
                        gsproc.PriorityClass = ProcessPriorityClass.Idle;
                    }
                }

                //
                // We need a "gong" to watch for hangs. gsview seems to do this with some annoying frequency!
                //

                var gong = new ProcessGong(gsproc.ProcessName, TimeToLetGSRun) { Watcher = new WatchDirForNewFiles(baseDirForFiles), CheckDelay = 1000 };
                gong.StartWait();

                ///
                /// Now monitor the i/o that comes back from there.
                /// 

                using (TextWriter wr = logInfo.CreateText())
                {
                    wr.WriteLine("Converting PDF to png");
                    wr.WriteLine("ghost script path: " + sinfo.FileName);
                    wr.WriteLine("Arguments: " + sinfo.Arguments);
                    wr.WriteLine("Log:");

                    string line = "hi";
                    while ((line = gsproc.StandardOutput.ReadLine()) != null)
                    {
                        line = line.Trim();
                        wr.WriteLine(line);
                        if (line.StartsWith("Error:"))
                        {
                            error_seen = line;
                        }
                    }
                    wr.Close();
                }

                ///
                /// Be good, wait for it to shut down - which should be seconds
                /// 

                gsproc.WaitForExit();
                gong.StopWait();
                endtime = DateTime.Now;
            }
            finally
            {
                _RenderLimiter.Release();
            }

            ///
            /// Find all the output jpeg images by scanning the directory.
            /// 

            int page_number = 1;
            bool done = false;
            List<FileInfo> result = new List<FileInfo>();
            while (!done)
            {
                FileInfo fn = new FileInfo(baseDirForFiles.FullName + "\\" + talkName + "-" + page_number.ToString("000") + ".png");
                done = !fn.Exists;
                if (!done)
                {
                    result.Add(fn);
                }
                page_number = page_number + 1;

                if (page_number > MaxPages)
                {
                    done = true;
                }
            }

            ///
            /// Log the result
            /// 

            if (error_seen == "")
            {
                MyErrorLogger.LogMessage("RenderUsingGS", "Finished rendering " + talkName);
            }
            else
            {
                MyErrorLogger.LogMessage("RenderUsingGS", "Finished rendering " + talkName + " - saw error: " + error_seen);
            }

            ///
            /// And create the token file, but only if there was no error processing it!
            /// 

            if (error_seen == "")
            {
                using (TextWriter wr = tokenInfo.CreateText())
                {
                    wr.WriteLine("Started: " + startime.ToShortTimeString());
                    wr.WriteLine("Done: " + endtime.ToShortTimeString());
                    wr.Close();
                }

                /// Cache the filelist for later use!

                using (TextWriter wr = filelistInfo.CreateText())
                {
                    foreach (var fname in result)
                    {
                        wr.WriteLine(fname);
                    }
                    wr.Close();
                }

                return result;
            }

            return new List<FileInfo>();
        }

        /// <summary>
        /// Make a best guess if we are looking at an EPS file. If it has the proper extension,
        /// and then is it has EPS in the header.
        /// </summary>
        /// <param name="talkInfo"></param>
        /// <returns></returns>
        private bool IsEPSFile(FileInfo talkInfo)
        {
            if (talkInfo.Extension.ToLower() == ".eps")
            {
                return true;
            }

            using (var reader = talkInfo.OpenText())
            {
                string line = reader.ReadLine();
                if (line.Contains("EPS"))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// We can do PS, EPS, and PDF files. But that is pretty much it!
        /// </summary>
        /// <param name="what"></param>
        /// <returns></returns>
        public RenderPluginPriority RenderPriority(object what)
        {
            FileInfo f = what as FileInfo;
            if (!f.Exists)
            {
                return RenderPluginPriority.RPPCantDoIt;
            }

            string ext = f.Extension.ToLower();

            if (ext == ".eps"
                || ext == ".ps"
                || ext == ".pdf")
            {
                return RenderPluginPriority.RPPGeneric;
            }
            return RenderPluginPriority.RPPCantDoIt;
        }
        /// <summary>
        /// Find the ghost script location.
        /// </summary>
        private void CacheGS()
        {
            if (GSLocation != null)
            {
                return;
            }

            using (RegistryKey k = Registry.LocalMachine.OpenSubKey("SOFTWARE\\GPL Ghostscript"))
            {
                if (k == null)
                {
                    ///
                    /// Last ditch effort to find things if the registry doesn't show us where it is... this is really fishing in the dark...
                    /// 

                    DirectoryInfo info = new DirectoryInfo(System.Environment.ExpandEnvironmentVariables(@"%SystemDrive%\Program Files\gs"));
                    if (info.Exists)
                    {
                        foreach (DirectoryInfo subdir in info.GetDirectories())
                        {
                            FileInfo gs = new FileInfo(subdir.FullName + @"\bin\gswin32c.exe");
                            if (!gs.Exists)
                            {
                                gs = new FileInfo(subdir.FullName + @"\bin\gswin64c.exe");
                            }
                            if (gs.Exists)
                            {
                                GSLocation = gs.FullName;
                                return;
                            }
                        }
                    }

                    ///
                    /// No luck!
                    /// 

                    throw new ApplicationException("Could not find ghost script registry key!");
                }
                foreach (var key in k.GetSubKeyNames())
                {
                    using (RegistryKey gsloc = k.OpenSubKey(key))
                    {
                        if (gsloc != null)
                        {
                            GSLocation = (string)gsloc.GetValue("GS_DLL");
                            if (GSLocation != null)
                            {
                                var f = new FileInfo(GSLocation.Substring(0, GSLocation.LastIndexOf("\\") + 1) + "gswin32c.exe");
                                if (f.Exists)
                                {
                                    GSLocation = f.FullName;
                                    return;
                                }
                                f = new FileInfo(GSLocation.Substring(0, GSLocation.LastIndexOf("\\") + 1) + "gswin64c.exe");
                                if (f.Exists)
                                {
                                    GSLocation = f.FullName;
                                    return;
                                }

                                throw new InvalidOperationException(string.Format("Unable to find gs located in {0}.", GSLocation));
                            }
                        }
                    }
                }

                throw new InvalidOperationException(string.Format("Unable to find Ghost Script locatoin, even though we found the registry key {0}", k.Name));
            }
        }

        /// <summary>
        /// Simple class to watch for changes in a directory
        /// </summary>
        private class WatchDirForNewFiles : ProcessGong.IWatchForChange
        {
            private DirectoryInfo _dirInfo;
            public WatchDirForNewFiles(DirectoryInfo dir)
            {
                this._dirInfo = dir;
            }

            /// <summary>
            /// Scan the directory for all files - anything updated, return it as a good file...
            /// </summary>
            /// <param name="lastChange"></param>
            /// <returns></returns>
            public bool HasChanged(DateTime lastChange)
            {
                var files = _dirInfo.EnumerateFiles();
                var bymodtime = from f in files
                                orderby f.LastWriteTime descending
                                select f;
                var bestFile = bymodtime.FirstOrDefault();
                if (bestFile == null)
                    return false;

                return bestFile.LastWriteTime > lastChange;
            }
        }

    }
}
